// Package repository
/**
* @Project : geektime-basic-go-study
* @File    : article.go
* @IDE     : GoLand
* @Author  : Tvux
* @Date    : 2024/11/9 10:50
**/

package article

import (
	"context"
	"geektime-basic-go-study/webook/backend/internal/domain"
	"geektime-basic-go-study/webook/backend/internal/repository/dao/article"
	"gorm.io/gorm"
	"time"
)

type ArticleRepository interface {
	Create(ctx context.Context, article domain.Article) (int64, error)
	UpdateByIdAndAuthorId(ctx context.Context, article domain.Article) error
	// Sync 存储并同步数据
	Sync(ctx context.Context, article domain.Article) (int64, error)
	SyncV1(ctx context.Context, article domain.Article) (int64, error)
	SyncV2(ctx context.Context, article domain.Article) (int64, error)
}

var _ ArticleRepository = (*CachedArticleRepository)(nil)

type CachedArticleRepository struct {
	dao article.ArticleDao

	// SyncV1
	authorDao article.ArticleAuthorDao
	readerDao article.ArticleReaderDao

	// SyncV2 耦合了 dao
	// 正常情况下，如果要在 repository 层面上操作事务
	// 那么只能利用 db 开始事务之后，创建基于事务的 dao
	// 或者，直接去掉 dao 这一层，在 repository 的实现中，直接操作 db
	db *gorm.DB
}

func (Self *CachedArticleRepository) Sync(ctx context.Context, article domain.Article) (int64, error) {
	return Self.dao.Sync(ctx, Self.domainToDao(article))
}

// SyncV2 考虑在 repository 层上解决事务问题
// 即保存到制作库和线上库要么都成功要么都失败
func (Self *CachedArticleRepository) SyncV2(ctx context.Context, art domain.Article) (int64, error) {
	// Begin 开启了一个事务
	tx := Self.db.WithContext(ctx).Begin()
	if tx.Error != nil {
		// 正常不会遇到这个错误，除非是数据库连接不够，或者数据库崩了
		return 0, tx.Error
	}
	// 遇到错误以及可能的panic都要回滚
	// 提交成功了也会回滚，但是此时回滚会返回一个错误，但是不需要去处理。
	defer tx.Rollback()

	author := article.NewArticleAuthorDao(tx)
	reader := article.NewArticleReaderDao(tx)

	var (
		id  = art.Id
		err error
	)
	articleDao := Self.domainToDao(art)
	if id > 0 {
		err = author.UpdateByIdAndAuthorId(ctx, articleDao)
	} else {
		id, err = author.Insert(ctx, articleDao)
	}
	if err != nil {
		return 0, err
	}
	// 确保线上库的文章id与制作库的相同
	art.Id = id
	const maxRetries = 3
	for i := 0; i < maxRetries; i++ {
		time.Sleep(time.Second * time.Duration(i+1)) // 简单的指数退避
		id, err = reader.UpsertV2(ctx, article.PublishedArticle{Article: articleDao})
		if err == nil {
			// 执行成功，提交
			tx.Commit()
			return id, err
		}
		//Self.Debug("保存到线上库失败，正在重试",
		//	logger.Field{Key: "article.Id", Val: article.Id},
		//	logger.Field{Key: "retry_count", Val: i + 1},
		//	logger.ErrorField(err),
		//)
	}
	//Self.Debug("重试彻底失败，无法保存到线上库",
	//	logger.Field{Key: "article.Id", Val: article.Id},
	//	logger.ErrorField(err),
	//)
	// 接入你的告警系统，手工处理一下
	// 走异步，我直接保存到本地文件
	// 走 Canal
	// 打 MQ
	return id, err
}

func (Self *CachedArticleRepository) SyncV1(ctx context.Context, article domain.Article) (int64, error) {
	var (
		id  = article.Id
		err error
	)
	articleDao := Self.domainToDao(article)
	if id > 0 {
		err = Self.authorDao.UpdateByIdAndAuthorId(ctx, articleDao)
	} else {
		id, err = Self.authorDao.Insert(ctx, articleDao)
	}
	if err != nil {
		return 0, err
	}
	// 确保线上库的文章id与制作库的相同
	article.Id = id
	const maxRetries = 3
	for i := 0; i < maxRetries; i++ {
		time.Sleep(time.Second * time.Duration(i+1)) // 简单的指数退避
		id, err = Self.readerDao.Upsert(ctx, articleDao)
		if err == nil {
			return id, err
		}
		//Self.Debug("保存到线上库失败，正在重试",
		//	logger.Field{Key: "article.Id", Val: article.Id},
		//	logger.Field{Key: "retry_count", Val: i + 1},
		//	logger.ErrorField(err),
		//)
	}
	//Self.Debug("重试彻底失败，无法保存到线上库",
	//	logger.Field{Key: "article.Id", Val: article.Id},
	//	logger.ErrorField(err),
	//)
	// 接入你的告警系统，手工处理一下
	// 走异步，我直接保存到本地文件
	// 走 Canal
	// 打 MQ
	return id, err
}

func (Self *CachedArticleRepository) UpdateByIdAndAuthorId(ctx context.Context, article domain.Article) error {
	return Self.dao.UpdateByIdAndAuthorId(ctx, Self.domainToDao(article))
}

func (Self *CachedArticleRepository) Create(ctx context.Context, article domain.Article) (int64, error) {
	return Self.dao.Insert(ctx, Self.domainToDao(article))
}

func NewCachedArticleRepository(dao article.ArticleDao) ArticleRepository {
	return &CachedArticleRepository{
		dao: dao,
	}
}

func (Self *CachedArticleRepository) daoToDomain(a article.Article) domain.Article {
	return domain.Article{
		Id:      a.Id,
		Title:   a.Title,
		Content: a.Content,
		Author: domain.Author{
			Id: a.AuthorId,
		},
		Status: domain.ArticleStatus(a.Status),
	}
}

func (Self *CachedArticleRepository) domainToDao(a domain.Article) article.Article {
	return article.Article{
		Id:       a.Id,
		Title:    a.Title,
		Content:  a.Content,
		AuthorId: a.Author.Id,
	}
}
